#define _GNU_SOURCE //pipe2()
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <paths.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <unistd.h>

#include "launcherd.h"

static int switchToUser( struct passwd* user ){
	
	/***   switch gid, groups and uid   ***/
	if( setgid(    user->pw_gid               ) ) goto l_perror;
	if( initgroups(user->pw_name, user->pw_gid) ) goto l_perror;
	if( setuid(    user->pw_uid               ) ) goto l_perror;
	
	/***   check they really switched   ***/
	if( getgid() != user->pw_gid || getegid() != user->pw_gid || 
		getuid() != user->pw_uid || geteuid() != user->pw_uid ){
		printf( "[launcherd] Failed switch uid or gid\n" );
		return -2;
	}
	
	/***   if target user is root, no more checks is needed   ***/
	if( !user->pw_uid || !user->pw_gid ) return 0;
	
	/***   extra check if target user not root   ***/
	if( !setuid(0) || !seteuid(0) ){
		printf( "[launcherd] Could not drop root privileges!\n" );
		return -3;
	}
	
	return 0;
	
l_perror:
	perror( "[launcherd] switch user" );
	return -1;
}

/**************************************************
func execute
	set environment, cd to home, 
	route stdin, stdout, stderr to /dev/null 
	and attempt to exec program from user
input
	user - user to run as
	programm - path to executable
return
	not return on success
	errno on error
**************************************************/
static int execute( struct passwd* user, char* programm, int pipe ){
	
	/***   args   ***/
	char* execArgs[] = { programm, NULL };
	
	/***   env   ***/
	char eUSER[64];
	char eLOGNAME[64];
	char eHOME[128];
	char eSHELL[128];
	char ePATH[] = "PATH=" _PATH_STDPATH;
	char* execEnv[] = { eUSER, eLOGNAME, eHOME, eSHELL, ePATH, NULL };
	
	if( strlen(user->pw_name) > 32 || strlen(user->pw_dir) > 120 || strlen(user->pw_shell) > 120 ){
		printf( "[launcherd] username or home or shell too long\n" );
		return ECANCELED;
	}
	
	sprintf( eUSER,    "USER=%s",    user->pw_name  );
	sprintf( eLOGNAME, "LOGNAME=%s", user->pw_name  );
	sprintf( eHOME,    "HOME=%s",    user->pw_dir   );
	sprintf( eSHELL,   "SHELL=%s",   user->pw_shell );
	
	/***   pwd   ***/
	if( chdir(user->pw_dir) ){
		printf( "[launcherd] Cannot cd to [%s], using [/] instead\n", user->pw_dir );
		if( chdir("/") ){
			printf( "[launcherd] chdir\n" );
			return ECANCELED;
		}
	}
	
	printf( "[launcherd] Trying to start [%s] from user [%s]\n", programm, user->pw_name );
	
	/***   route stdin, stdout and stderr to /dev/null   ***/
	if( fileno( freopen(_PATH_DEVNULL, "rb", stdin ) ) != 0 ) return EBADF;
	if( fileno( freopen(_PATH_DEVNULL, "wb", stdout) ) != 1 ) return EBADF;
	if( fileno( freopen(_PATH_DEVNULL, "wb", stderr) ) != 2 ) return EBADF;
	
	/***   exec   ***/
	execve( programm, execArgs, execEnv );
	return errno;
}

/**************************************
func executeFromUser
	switch to user and execute prorgamm
return
	not return on success
	if return then error
**************************************/
static int executeFromUser( struct launcherdRequest* execrq, int pipe ){
	
	int savedErrno;
	struct passwd* user;
	
	//get user data from /etc/passwd
	user = getpwnam( execrq->username );
	if( !user ){
		if( !errno ) printf( "[launcherd] User [%s] not found.\n", execrq->username );
		else perror( "[launcherd] getpwnam" );
		savedErrno = ECANCELED;
		goto l_error;
	}
	
	//switch to user
	if( switchToUser(user) ){
		savedErrno = ECANCELED;
		goto l_error;
	}
	
	//try to execute;
	savedErrno = execute( user, execrq->programm, pipe );
	
l_error:
	return write( pipe, &savedErrno, sizeof(savedErrno) );
}

/**********************************************
func launcherdListen
	listen control socket and handle connection
input
	mySocket - socket to listen
return
	0 on exit if socket close from other side
	not 0 on error
**********************************************/
static int launcherdListen( int mySocket ){
	
	int res;
	int execRes;
	int pipes[2];
	struct launcherdRequest rq;
	
	printf( "[launcherd] Listen\n" );
	
l_listen:
	//receive request from socket
	if( recv(mySocket, &rq, sizeof(rq), MSG_WAITALL) != sizeof(rq) ){
		if( errno ){
			perror( "[launcherd] recv" );
			return -1;
		}
		printf( "[launcherd] Socket seems has been closed. Exiting.\n" );
		return 0;
	}
	
	//strings must be null-terminated
	rq.username[sizeof(rq.username)-1] = 0x00;
	rq.programm[sizeof(rq.programm)-1] = 0x00;
	
	//create pipes to check execute result
	res = pipe2( pipes, O_CLOEXEC );
	if( res ){
		perror( "[launcherd] pipe2" );
		return -2;
	}
	
	//fork process to execute requested program
	res = fork();
	if( res < 0 ){
		perror( "[launcherd] fork" );
		return -3;
	}
	
	//prepare and exec in the child process
	if( !res ){//child
		close( mySocket );
		close( pipes[0] );
		executeFromUser( &rq, pipes[1] );
		exit( 1 );//exit if exec failed
	}
	
	//close child pipe
	close( pipes[1] );
	
	//read and print execute result from child over pipe
	res = read( pipes[0], &execRes, sizeof(execRes) );
	if( res != sizeof(execRes) ) printf( "[launcherd] Success.\n" );
	else printf( "[launcherd] Failed: %s.\n", strerror(execRes) );
	
	//close our pipe
	close( pipes[0] );
	
	//listen next request
	goto l_listen;
}

/********************************************
func launcherdStart
	create control socket and start launcherd
return 
	negative on error
	positive as control socket
********************************************/
int launcherdStart( void ){
	
	int res;
	int sockets[2];
	
	//create sockets
	res = socketpair( AF_UNIX, SOCK_STREAM, 0, sockets );
	if( res ){
		perror( "[launcherd] socketpair" );
		return -1;
	}
	
	//fork process
	res = fork();
	if( res < 0 ){
		perror( "[launcherd] fork" );
		return -2;
	}
	
	//start listen socket in the child process
	if( !res ){//child
		close( sockets[0] );
		prctl( PR_SET_NAME, "launcherd", 0, 0, 0 );
		signal( SIGCHLD, SIG_IGN );
		signal( SIGPIPE, SIG_IGN );
		res = launcherdListen( sockets[1] );
		printf( "[launcherd] Exiting with code: %i\n", res );
		close( sockets[1] );
		exit( res );
	}
	
	//return socket for communicate with child process
	close( sockets[1] );
	return sockets[0];
}
